package client;

import client.gui.ClientGUI_Lobby;
import client.gui.ClientGUI_Game;
import com.cedarsoftware.util.io.JsonReader;
import com.cedarsoftware.util.io.JsonWriter;
import shared.CompObj;
import shared.Consts;
import shared.Consts.GameState;
import shared.PlayerPortfolio;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import network.Q2NetRunnable;
import packet.ChatReq;
import packet.Command;
import packet.CommandRequest;
import packet.InitGameReq;
import packet.LobbyJoinReq;
import packet.NextTurnReq;
import packet.PlayerJoinReq;
import packet.PlayerQuitReq;
import packet.Request;

/**
 * Client controller<br/>
 * Holds client end GUIs and networking
 * @author Joel Garboden
 */
public class ClientController
{
  public String playerName;
  public int playerID;

  private ObjectOutputStream socketOut;
  private ObjectInputStream socketIn;
  
  JsonWriter jsonOut;
  JsonReader jsonIn;
  
  PriorityBlockingQueue<CommandRequest> inQ;
  PriorityBlockingQueue<CommandRequest> outQ;


  GameState state;

  private ClientGUI_Lobby lobbyGui;

  public ClientGUI_Game gameGui;

  private ClientListener listener;

  /**
   * Constructor
   * @param outQ queue for outgoing network traffic
   */
  public ClientController(PriorityBlockingQueue<CommandRequest> outQ)
  {
    this.outQ = outQ;
  }

  /**
   * Sets lobby state dependencies
   * @param inQ queue for incoming network traffic
   * @param lobbyGui lobby state gui reference
   */
  public void setDependencies(PriorityBlockingQueue<CommandRequest> inQ, ClientGUI_Lobby lobbyGui)
  {
    this.lobbyGui = lobbyGui;
    state = GameState.LOBBY_MODE;

    this.inQ = inQ;
  }

  /**
   * Sets game state dependencies
   * @param inQ queue for incoming network traffic
   * @param gameGui game state gui reference
   */
  public void setDependencies(PriorityBlockingQueue<CommandRequest> inQ, ClientGUI_Game gameGui)
  {
    this.gameGui = gameGui;
    state = GameState.GAME_MODE;

    this.inQ = inQ;    
  }

  /**
   * Mutator
   * @param playerName name to be set to
   */
  public void setPlayerName(String playerName)
  {
    this.playerName = playerName;
  }

  public void setPlayerID(int ID)
  {
    playerID = ID;
  }

  public void nameRequest(int playerID)
  {
    sendRequest(new PlayerJoinReq(playerID, playerName));
  }

  /**
   * Accessor
   * @return reference to current clint controller (this)
   */
  public ClientController getClientController()
  {
    return this;
  }

  /**
   * Adds a chat message onto the outgoing queue
   * @param message chat message to be sent
   */
  public void sendChat(String message)
  {
    sendRequest(new ChatReq(playerName, message));
  }

  /**
   * Posts chat message to proper gui
   * @param playerName sending player
   * @param message incoming message
   */
  public void postChat(String playerName, String message)
  {
    if(state == GameState.LOBBY_MODE)
    {
      lobbyGui.postChat(playerName, message);
    }
    else
    {
      gameGui.postChat(playerName, message);
    }
  }

  /**
   * Starts client incoming network listener
   * @param in the stream to listen on
   */
  public void startListener(JsonReader in)
  {
    jsonIn = in;
    listener = new ClientListener(in);
    listener.start();
  }

  /**
   * Starts client ourgoing network sender
   * @param out the stream to send packets over
   */
  public void startSender(String playerName, JsonWriter out) throws IOException
  {
    jsonOut = out;
    Runnable rOut = new Q2NetRunnable(playerName, jsonOut, outQ);
    Thread tOut = new Thread(rOut);
    tOut.start();
  }

  /**
   * Adds a request to the outgoing queue
   * @param outgoing the outgoing request
   */
  public void sendRequest(Request outgoing)
  {
    outQ.add(outgoing);
  }

  /**
   * Connects to a game instance
   * @param ipAddress game host's IP address
   * @param port port host is listening on
   */
  public void invitedToGame(String ipAddress, int port, int gameID)
  {
    int n = JOptionPane.showConfirmDialog(lobbyGui, Consts.INVITE_MSG,
                                          "Game invitation",
                                          JOptionPane.YES_NO_OPTION);

    if(n == JOptionPane.NO_OPTION)
    {
      return;
    }

    PriorityBlockingQueue<CommandRequest> outGoingQ = new PriorityBlockingQueue<>
                                                        (Consts.CLIENT_QUEUE_SIZE, new CompObj());
    PriorityBlockingQueue<CommandRequest> inComingQ = new PriorityBlockingQueue<>
                                                        (Consts.CLIENT_QUEUE_SIZE, new CompObj());

    ClientController aGameController = new ClientController(outGoingQ);

    ClientGUI_Game newGameGUI = new ClientGUI_Game(aGameController, gameID);

    aGameController.setDependencies(inComingQ, newGameGUI);

    aGameController.setPlayerName(playerName);

    aGameController.connectToServer(playerName, ipAddress, port);

    aGameController.gameGui.setVisible(true);
  }

  /**
   *
   */
  public void finishTurn()
  {
    if(!gameGui.gameStarted)
    {
      sendRequest(new InitGameReq(playerName));
    }
    else
    {
      sendRequest(new NextTurnReq());
    }
  }

  /**
   * Sends a request to the server to authorize quitting
   */
  public void quit()
  {
    sendRequest(new PlayerQuitReq(playerName));
  }

  /**
   * Disconnects and closes all streams
   */
  public void disconnect()
  {
    try
    {
      if(listener != null)
      {
        listener.stopNow();
      }
      if(socketOut != null)
      {
        socketOut.close();
      }
      if(socketIn != null)
      {
        socketIn.close();
      }
      if(jsonIn != null)
      {
        jsonIn.close();
      }      
      if(jsonOut != null)
      {
        jsonOut.close();
      }      
      

    }
    catch (Exception ex)
    {
      Logger.getLogger(ClientController.class.getName()).log(Level.SEVERE, "disconnect", ex);
    }
  }

  /**
   * Connects client to specified host lobby
   * @param hostIP lobby server IP
   * @param port lobby server listen port
   * @return connection success/failure
   */
  public boolean connectToServer(String playerName, String hostIP, int port)
  {
    Socket socClient = null;
 
    try
    {
      socClient = new Socket(hostIP, port);
      if(socClient.isConnected())
      {
        socketOut = new ObjectOutputStream(socClient.getOutputStream());
        jsonOut = new JsonWriter(socketOut);
        
        socketIn = new ObjectInputStream(socClient.getInputStream());
        jsonIn = new JsonReader(socketIn);
        
        if(state == GameState.LOBBY_MODE)
        {
          //socketOut.writeObject(new LobbyJoinReq(playerName));
          jsonOut.write(new LobbyJoinReq(playerName));
        }

        //Command cmd = (Command)socketIn.readObject();
        Command cmd = (Command)jsonIn.readObject();
        //while(!(cmd instanceof PlayerJoinCmd) && !(cmd instanceof LobbyJoinCmd))
        //{
          //cmd = (Command)socketIn.readObject();
          //inQ.add(cmd);
        //}

        cmd.execCommand(this);

        startListener(jsonIn);

        startSender(playerName, jsonOut);

        if(state == GameState.LOBBY_MODE)
        {
          lobbyGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_MSG);
        }
        else
        {
          gameGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_MSG);
        }
        return true;
      }
      else
      {
        if(state == GameState.LOBBY_MODE)
        {
          lobbyGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_ERR_MSG);
        }
        else
        {
          gameGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_ERR_MSG);
        }
        return false;
      }
    }
    catch(IOException e)  //IOException and ClassNotFoundException
    {
      Logger.getLogger(ClientGUI_Lobby.class.getName()).log(Level.SEVERE, "ClientController", e);

      if(state == GameState.LOBBY_MODE)
      {
        lobbyGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_ERR_MSG);
        lobbyGui.toggleChat(false);
      }
      else
      {
        gameGui.postChat(Consts.SYSTEM_NAME, Consts.CONN_ERR_MSG);
        gameGui.toggleChat(false);
      }
      try
      {
        if(socClient != null)
        {
          socClient.close();
        }
      }
      catch (Exception ex)
      {
        Logger.getLogger(ClientGUI_Lobby.class.getName()).log(Level.SEVERE, "connect inner", ex);
      }
    }

    return false;
  }

  /**
   * Mutator
   * @param turnToggle turn state
   */
  public void toggleTurn(boolean turnToggle)
  {
    if(state == GameState.GAME_MODE)
    {
      gameGui.toggleTurn(turnToggle);
    }
  }

  /**
   *
   * @param portfolio
   */
  public void updatePortfolio(PlayerPortfolio portfolio)
  {
    if(state == GameState.GAME_MODE)
    {
      gameGui.setPortfolio(portfolio);
      gameGui.updateData();
    }
    else
    {
      System.err.println("Invalid command for non-game mode");
    }
  }

  public void gameInitiated(PlayerPortfolio portfolio)
  {
    gameGui.setPortfolio(portfolio);
    gameGui.gameStarted = true;
    gameGui.finishTurn.setText("Finish turn");
    gameGui.updateData();
  }

  /**
   *
   * @return
   */
  public boolean isGameStarted()
  {
    return gameGui.gameStarted;
  }

  public void firstTurn()
  {
    gameGui.finishTurn.setText("Begin game");
    gameGui.toggleTurn(true);
  }

  /**
   * ClientController.java
   * Listens to an incoming network stream on a separate thread
   */
  public class ClientListener extends Thread
  {
    JsonReader receiving;
    boolean listen;

    /**
     * Constructor
     * @param in incoming stream to listen on
     */
    public ClientListener(JsonReader in)
    {
      listen = true;
      receiving = in;
    }

    /**
     * Attempts to stop the stream completely
     */
    public void stopNow()
    {
      try
      {
        listen = false;
        interrupt();
      }
      catch (Throwable ex)
      {
        Logger.getLogger(ClientController.class.getName()).log(Level.SEVERE, "ClientListener", ex);
      }
    }

    /**
     * Continually listens on incoming network stream and <br/>
     * drops packets to queue for processing
     */
    @Override
    public void run()
    {
      Command cmd;
      while(listen)
      {
        try
        {
          if(receiving == null)
          {
            return;
          }

          cmd = (Command)receiving.readObject();
          inQ.add(cmd);
          //System.out.println("ClientListener - inQ.add " + cmd.toString());
          java.awt.EventQueue.invokeLater(new Runnable()
          {
            @Override
            public void run()
            {
              try
              {
                inQ.take().execCommand(getClientController());
              }
              catch (InterruptedException ex)
              {
                Logger.getLogger(ClientListener.class.getName()).log(Level.SEVERE, "ClientListener inner", ex);
              }
            }
          });
        }
        catch (IOException ex) //IOException and ClassNotFoundException
        {
          Logger.getLogger(ClientListener.class.getName()).log(Level.SEVERE, "ClientListener outer", ex);
          return;
        }
      }
    }
  }
}
